package org.texteasy;

import java.util.ArrayList;

import android.content.ContentProviderOperation;
import android.content.ContentResolver;
import android.content.OperationApplicationException;
import android.content.ContentProviderOperation.Builder;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.RemoteException;
import android.provider.ContactsContract;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.Groups;
import android.provider.ContactsContract.RawContacts;
import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.util.Log;
import android.util.Pair;

/**
 * Utility class that performs the work of synchronizing group selections
 * with contacts.
 * @author klewelling
 *
 */
public final class SynchronizeGroupsAndContacts {

	public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/text_easy_group";
	
	private static final int MAX_RECORDS_TO_PERSIST_BATCHSIZE = 200;
	
	public static synchronized void synchronizeGroupsAndContacts(ContentResolver contentResolver, 
			ArrayList<Long> groupIds, ArrayList<Pair<String,String>> ungroupedSelections, 
			TextEasyDBAdapter dbAdapter){
		
		/*Cursor existingSelections = contentResolver.query(Data.CONTENT_URI, 
			new String[]{Data._ID, Data.RAW_CONTACT_ID, Data.CONTACT_ID}, 
			Data.MIMETYPE + "=?",new String[]{CONTENT_ITEM_TYPE}, 
			Data.RAW_CONTACT_ID);
		
		Cursor inSelectedGroups = contentResolver.query(Data.CONTENT_URI, 
			new String[]{GroupMembership.GROUP_ROW_ID, GroupMembership.RAW_CONTACT_ID, GroupMembership.CONTACT_ID}, 
			Data.MIMETYPE + "=?", new String[]{GroupMembership.CONTENT_ITEM_TYPE}, Data.RAW_CONTACT_ID);
		
		//ungroup is a contact that is not in a group. This is the case when 
		Cursor inSelectedUngroup = contentResolver.query(RawContacts.CONTENT_URI, 
			RawContacts._ID, RawContacts.CONTACT_ID, selection, selectionArgs, sortOrder) 
		//ContactsContract.RawContacts.ACCOUNT_NAME
		
		try{
		while(true){
			
			boolean anotherExistingSelection = existingSelections.moveToNext();
			boolean anotherSelectedGroup = inSelectedGroups.moveToNext();
			
			if(anotherExistingSelection){
				Log.i(SynchronizeGroupsAndContacts.class.getSimpleName(), "Currently selected contactId: " + 
					existingSelections.getInt(1));
			}else{
				Log.i(SynchronizeGroupsAndContacts.class.getSimpleName(), "No more existing contacts");
			}
			
			if(anotherSelectedGroup){
				Log.i(SynchronizeGroupsAndContacts.class.getSimpleName(), "In selected group contactId: " + 
						inSelectedGroups.getInt(1));
			}else{
				Log.i(SynchronizeGroupsAndContacts.class.getSimpleName(), "No more selected group contacts");
			}
			
			if(!anotherExistingSelection && !anotherSelectedGroup){
				break;
			}
			
		}
		}finally{
			if(existingSelections != null){q
				existingSelections.close();
			}
			
			if(inSelectedGroups != null){
				inSelectedGroups.close();
			}
		}
		*/
		
		dbAdapter.clearTempMatchingContacts();
		loadGroupSelections(contentResolver, groupIds, dbAdapter);
		loadUnGroupSelections(contentResolver,ungroupedSelections, dbAdapter);
		
		synchronizeMatchedContactsWithContactsData(contentResolver, dbAdapter);
	}
	
	private static void synchronizeMatchedContactsWithContactsData(
		ContentResolver contentResolver,TextEasyDBAdapter dbAdapter){
		
		//need two queries
		//1) Contacts that currently have Text-Easy selected contact data
		//2) Matching contacts
		SQLiteDatabase db = null;
		Cursor existingSelections = null;
		Cursor matchingContacts = null;
		try{
			db = dbAdapter.getReadableDatabase();
			
			long matchingContactId = Long.MIN_VALUE;
			long existingContactId = Long.MIN_VALUE;
			matchingContacts = dbAdapter.queryMatchingContacts(db);
			ContactDataOperation dataOperation = new ContactDataOperation(contentResolver);
			
			existingSelections = contentResolver.query(Data.CONTENT_URI, 
					new String[]{Data._ID, Data.RAW_CONTACT_ID, Data.CONTACT_ID}, 
					Data.MIMETYPE + "=?",new String[]{CONTENT_ITEM_TYPE}, 
					Data.CONTACT_ID);
			
			while(matchingContacts.moveToNext()){
				
				long contactId = matchingContacts.getLong(0);
				long rawContactId = matchingContacts.getLong(1);
				
				//it is possible that a contact could be in the matchingContacts
				//cursor more than once (example contact with two raw contacts from
				//different accounts are both matching), if this contact is 
				//the same as the last move to the next contact
				if(contactId == matchingContactId){
					continue;
				}
				
				matchingContactId = contactId;
				
				
				//check if its already selected
				//note: even though TextEasy selection row is associated with a raw_contact_id
				//only look at the contactId when determining if a existing selection should
				//remain or be removed. 
				if(existingContactId > matchingContactId){
					//insert the matchingContactId
					dataOperation.add(Pair.create(rawContactId, OperationEnum.INSERT));
				}else if(existingContactId < matchingContactId){
					
					//iterate over the existingContacts, deleting all we find until
					//we find one >=
					boolean addMatching = true;
					while(existingSelections.moveToNext()){
						
						existingContactId = existingSelections.getLong(2);
						long existingRawContactId = existingSelections.getLong(1);
						
						if(existingContactId == matchingContactId){
							//the matching contactId already has a data row so move to the 
							//next matchingContact
							addMatching = false;
							break;
						}else if(existingContactId < matchingContactId){
							//found existingContactId that needs to be removed
							dataOperation.add(Pair.create(existingRawContactId, OperationEnum.DELETE));
						}else{
							//pass matchingContactId, which means need to add matching to be inserted
							addMatching  = true;
							break;
						}
					}
					
					//need addMatching because there may not be any more contacts in
					//existingSelections. If there are no more that means the matching contact
					//should be inserted
					if(addMatching){
						dataOperation.add(Pair.create(rawContactId, OperationEnum.INSERT));
					}
				}
			}
			
			//iterate over remaining current contacts, deleting any you find
			while(existingSelections.moveToNext()){
				Long existingRawContactId = existingSelections.getLong(1);
				dataOperation.add(Pair.create(existingRawContactId, OperationEnum.DELETE));
			}
			
			dataOperation.persistContactData();
		}finally{
			
			if(matchingContacts != null){
				matchingContacts.close();
			}
			
			if(db != null){
				db.close();
			}
			
			if(existingSelections != null){
				existingSelections.close();
			}
		}
		
	}
	
	private static void loadUnGroupSelections(ContentResolver contentResolver,
			ArrayList<Pair<String,String>> ungroupedSelections,
			TextEasyDBAdapter dbAdapter){
		
		if(ungroupedSelections.isEmpty()){
			return;
		}
		
		//have no direct way to find ungrouped selections in an account, so have to do two queries, 
		//the first has all contacts in an account, the second has all contacts assocaited to a group 
		//in an account. Store the difference
		for(Pair<String,String> ungroupedSelection: ungroupedSelections){
		
			//first need to figure out what groups an account has
			Cursor groupsReleatedToAccount = contentResolver.query(Groups.CONTENT_URI, 
				new String[]{Groups._ID}, Groups.ACCOUNT_NAME + "=? AND " + 
				Groups.ACCOUNT_TYPE + "=?", new String[]{ungroupedSelection.first, 
					ungroupedSelection.second}, null);
			
			ArrayList<Long> groupIds = new ArrayList<Long>();
			
			try{
				
				while(groupsReleatedToAccount.moveToNext()){
					groupIds.add(groupsReleatedToAccount.getLong(0));
				}
				
			}finally{
				if(groupsReleatedToAccount != null){
					groupsReleatedToAccount.close();
				}
			}
			
			/*Cursor test = contentResolver.query(RawContacts.CONTENT_URI, 
					new String[]{RawContacts._ID, RawContacts.CONTACT_ID},
					RawContacts.ACCOUNT_NAME + "=? AND " +
					RawContacts.ACCOUNT_TYPE + "=?", new String[]{ungroupedSelection.first, 
					ungroupedSelection.second},RawContacts._ID);
			
			while(test.moveToNext()){
				
				Long rawContactId = test.getLong(0);
				Long contactId = test.getLong(1);
				
				Cursor test2 = contentResolver.query(Contacts.CONTENT_URI, new String[]{Contacts.DISPLAY_NAME}, 
						Contacts._ID + "=?", new String[]{contactId.toString()}, Contacts._ID);
				
				Log.i(SynchronizeGroupsAndContacts.class.getSimpleName(), "Raw: " + rawContactId + 
						" contactId: " + contactId );
				while(test2.moveToNext()){
					Log.i(SynchronizeGroupsAndContacts.class.getSimpleName(), "Raw: " + rawContactId + 
							" contactId: " + contactId + " displayname: " + 
							test2.getString(0));
				}
				test2.close();
			}
			
			test.close();*/
			
			//if an account has no groups then its simple, 
			//load all the contacts
			Cursor allContactsInGroup = contentResolver.query(RawContacts.CONTENT_URI, 
				new String[]{RawContacts._ID, RawContacts.CONTACT_ID},
				RawContacts.ACCOUNT_NAME + "=? AND " +
				RawContacts.ACCOUNT_TYPE + "=?", new String[]{ungroupedSelection.first, 
				ungroupedSelection.second},RawContacts._ID);
			
			//need allContactsWithPhoneNumber to make sure only contacts
			//with phone numbers make it in the database. If we don't do this check
			//then a rawContact not in a group without a phone # will be inserted. When adding
			//the Data row the phone number check may pass due to the rawContact being associated with
			//another rawContact (in a group) with a phone #. In this case a group contact will 
			//be show when it should not have been
			//TODO: filter by type
			Cursor allContactsWithPhoneNumber = contentResolver.query(Phone.CONTENT_URI,
				new String[]{Phone.RAW_CONTACT_ID, Phone.CONTACT_ID, Phone.NUMBER}, null, null, 
				Phone.RAW_CONTACT_ID);
			
			if(groupIds.isEmpty()){
				loadAllContactsFromAccount(allContactsInGroup,allContactsWithPhoneNumber, dbAdapter, contentResolver);
			}else{
				loadUngroupedContactsFromAccount(contentResolver, allContactsInGroup, 
					allContactsWithPhoneNumber, groupIds, dbAdapter);
			}
		}
		
	}
	
	private static void loadUngroupedContactsFromAccount(
		ContentResolver contentResolver, Cursor allContacts, 
		Cursor allContactsWithPhoneNumber, ArrayList<Long> groupIds,
		TextEasyDBAdapter dbAdapter){

		String groupInStatement = getGroupsInStatement(groupIds);
		
		Cursor contactsInGroups = contentResolver.query(Data.CONTENT_URI, 
			new String[]{GroupMembership.GROUP_ROW_ID, GroupMembership.RAW_CONTACT_ID, 
			GroupMembership.CONTACT_ID},Data.MIMETYPE + "=? AND " + 
			GroupMembership.GROUP_ROW_ID + " in (" + 
			groupInStatement + ")", new String[]{GroupMembership.CONTENT_ITEM_TYPE}, 
			GroupMembership.RAW_CONTACT_ID);
		
		try{

			MatchingContacts matchingContacts = new MatchingContacts(dbAdapter);
			long contactWithNumber = Long.MIN_VALUE;
			long rawContactId = Long.MIN_VALUE;
			
			while(contactsInGroups.moveToNext()){
				long tempRawContactId = contactsInGroups.getLong(1);
				//contactsInGroups can return the same data multiple times, be sure to skip over it
				if(tempRawContactId == rawContactId){
					continue;
				}
				rawContactId = tempRawContactId;
				//iterate over allContacts, add all contacts we find until
				//we hit the rawContactId
				while(allContacts.moveToNext()){
					long acRawContactId = allContacts.getLong(0);
					if(acRawContactId == rawContactId){
						break;
					}
					
					//make sure the contact has a phone number
					if(contactWithNumber == acRawContactId){
						
						logContactDebugInfo(contentResolver, acRawContactId, allContacts.getLong(1));
						
						matchingContacts.add(Pair.create(allContacts.getLong(1), 
								allContacts.getLong(0)));
					}else if(contactWithNumber < acRawContactId){
						
						while(allContactsWithPhoneNumber.moveToNext()){
							
							long tempContactWithNumber = allContactsWithPhoneNumber.getLong(0);
							//get mutliple results for one rawContactId, skip the dups 
							if(tempContactWithNumber == contactWithNumber){
								continue;
							}
							
							contactWithNumber = tempContactWithNumber;
							
							if(contactWithNumber == acRawContactId){
								
								logContactDebugInfo(contentResolver, acRawContactId, allContacts.getLong(1));
								
								matchingContacts.add(Pair.create(allContacts.getLong(1), 
										allContacts.getLong(0)));
							}else if(contactWithNumber > acRawContactId){
								break;
							}
						}
					}
					
					
					/*Log.i(SynchronizeGroupsAndContacts.class.getName(), "ContactId: " + 
						allContacts.getLong(1) + " Raw ContactId: " + 
						allContacts.getLong(0));*/
					
				}
			}
			
			//have made it through all cotactsInGroups. Any remaining contacts in allContacts
			//should be added
			while(allContacts.moveToNext()){
				
				long acRawContactId = allContacts.getLong(0);
				
				//make sure the contact has a phone number
				if(contactWithNumber == acRawContactId){
					matchingContacts.add(Pair.create(allContacts.getLong(1), 
							allContacts.getLong(0)));
				}else if(contactWithNumber < acRawContactId){
					
					while(allContactsWithPhoneNumber.moveToNext()){
						
						long tempContactWithNumber = allContactsWithPhoneNumber.getLong(0);
						//get mutliple results for one rawContactId, skip the dups 
						if(tempContactWithNumber == contactWithNumber){
							continue;
						}
						
						contactWithNumber = tempContactWithNumber;
						
						if(contactWithNumber == acRawContactId){
							matchingContacts.add(Pair.create(allContacts.getLong(1), 
									allContacts.getLong(0)));
						}else if(contactWithNumber > acRawContactId){
							break;
						}
					}
				}
				
				
				/*Log.i(SynchronizeGroupsAndContacts.class.getName(), "ContactId: " + 
						allContacts.getLong(1) + " Raw ContactId: " + 
						allContacts.getLong(0));*/
			}
			
			//persist any remaining matching contacts
			matchingContacts.persistMatchingContacts();
			
		}finally{
			if(allContacts != null){
				allContacts.close();
			}
			
			if(contactsInGroups != null){
				contactsInGroups.close();
			}
			
			if(allContactsWithPhoneNumber != null){
				allContactsWithPhoneNumber.close();
			}
		}
		
	}
	
	private static void loadAllContactsFromAccount(Cursor allContacts,
		Cursor allContactsWithPhoneNumber, TextEasyDBAdapter dbAdapter, ContentResolver contentResolver){
		
		try{
			
			MatchingContacts matchingContacts = new MatchingContacts(dbAdapter);
			
			long contactWithNumber = Long.MIN_VALUE;
			
			while(allContacts.moveToNext()){
				
				long rawId = allContacts.getLong(0);
				
				//logContactDebugInfo(contentResolver, rawId, allContacts.getLong(1));
				
				if(contactWithNumber == rawId){
					matchingContacts.add(Pair.create(allContacts.getLong(1), 
							allContacts.getLong(0)));
				}else if(contactWithNumber < rawId){
					
					while(allContactsWithPhoneNumber.moveToNext()){
						
						long tempContactWithNumber = allContactsWithPhoneNumber.getLong(0);
						//get mutliple results for one rawContactId, skip the dups 
						if(tempContactWithNumber == contactWithNumber){
							continue;
						}
						
						contactWithNumber = tempContactWithNumber;
						
						if(contactWithNumber == rawId){
							matchingContacts.add(Pair.create(allContacts.getLong(1), 
									allContacts.getLong(0)));
						}else if(contactWithNumber > rawId){
							break;
						}
					}
				}
				
				/*Log.i(SynchronizeGroupsAndContacts.class.getName(), "ContactId: " + 
						allContacts.getLong(1) + " Raw ContactId: " + 
						allContacts.getLong(0));*/

			}
			
			//persist remaining contacts
			matchingContacts.persistMatchingContacts();
		}finally{
			if(allContacts != null){
				allContacts.close();
			}
			
			if(allContactsWithPhoneNumber != null){
				allContactsWithPhoneNumber.close();
			}
		}
		
	}
	
	private static void loadGroupSelections(ContentResolver contentResolver, 
			ArrayList<Long> groupIds,TextEasyDBAdapter dbAdapter){
		
		if(groupIds.isEmpty()){
			return;
		}
		
		final String groupInStatement = getGroupsInStatement(groupIds);
		
		//need all phone numbers by filtered type
		//TODO: filter by type
		Cursor allContactsWithPhoneNumber = contentResolver.query(Phone.CONTENT_URI,
			new String[]{Phone.RAW_CONTACT_ID, Phone.CONTACT_ID}, null, null, 
			Phone.RAW_CONTACT_ID);
		
		Cursor inSelectedGroups = contentResolver.query(Data.CONTENT_URI, 
			new String[]{GroupMembership.GROUP_ROW_ID, GroupMembership.RAW_CONTACT_ID, 
			GroupMembership.CONTACT_ID}, 
			Data.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + " in (" + 
			groupInStatement + ")", new String[]{GroupMembership.CONTENT_ITEM_TYPE}, null);
		
		try{
			
			final MatchingContacts matchingContacts = new MatchingContacts(dbAdapter);
			
			long contactWithNumber = Long.MIN_VALUE;
			
			while(inSelectedGroups.moveToNext()){
				
				long rawId = inSelectedGroups.getLong(1);
				
				if(contactWithNumber == rawId){
					matchingContacts.add(Pair.create(inSelectedGroups.getLong(2), 
							inSelectedGroups.getLong(1)));
				}else if(contactWithNumber < rawId){
					
					while(allContactsWithPhoneNumber.moveToNext()){
						
						long tempContactWithNumber = allContactsWithPhoneNumber.getLong(0);
						//get mutliple results for one rawContactId, skip the dups 
						if(tempContactWithNumber == contactWithNumber){
							continue;
						}
						
						contactWithNumber = tempContactWithNumber;
						
						if(contactWithNumber == rawId){
							matchingContacts.add(Pair.create(inSelectedGroups.getLong(2), 
								inSelectedGroups.getLong(1)));
						}else if(contactWithNumber > rawId){
							break;
						}
					}
				}
				
				/*Log.i(SynchronizeGroupsAndContacts.class.getName(), "ContactId: " + 
					inSelectedGroups.getLong(2) + " Raw ContactId: " + inSelectedGroups.getLong(1));*/
			}
			
			//persist any contacts in the list
			matchingContacts.persistMatchingContacts();
			
		}finally{
			if(inSelectedGroups != null){
				inSelectedGroups.close();
			}
			
			if(allContactsWithPhoneNumber != null){
				allContactsWithPhoneNumber.close();
			}
		}
		
	}

	private static String getGroupsInStatement(ArrayList<Long> groupIds) {
		final StringBuilder stringBuilder = new StringBuilder();
		for(Long groupId: groupIds){
			stringBuilder.append(groupId).append(",");
		}
		
		String groupInStatement = stringBuilder.substring(0, stringBuilder.length() -1);
		return groupInStatement;
	}
	
	private static final class MatchingContacts extends ArrayList<Pair<Long,Long>>{
		
		private static final long serialVersionUID = -1052342684496027010L;
		
		private final TextEasyDBAdapter dbAdapter;
		
		public MatchingContacts(TextEasyDBAdapter dbAdapter){
			super(MAX_RECORDS_TO_PERSIST_BATCHSIZE);
			if(dbAdapter == null){
				throw new IllegalArgumentException("This method does not accept null parameters.");
			}
			this.dbAdapter = dbAdapter;
		}
		
		@Override
		public boolean add(Pair<Long, Long> object) {
			boolean toReturn = super.add(object);
			
			if(size() >= MAX_RECORDS_TO_PERSIST_BATCHSIZE){
				dbAdapter.persistMatchingContacts(this);
				clear();
			}
			return toReturn;
		}
		
		public void persistMatchingContacts(){
			dbAdapter.persistMatchingContacts(this);
			clear();
		}
	}
	
	private static final class ContactDataOperation extends ArrayList<Pair<Long,OperationEnum>>{
		
		private static final long serialVersionUID = -3363379882351972440L;
		
		private final ContentResolver contentResolver;
		
		public ContactDataOperation(ContentResolver contentResolver){
			super(MAX_RECORDS_TO_PERSIST_BATCHSIZE);
			if(contentResolver == null){
				throw new IllegalArgumentException("This method does not accept null parameters.");
			}
			this.contentResolver = contentResolver;
		}
		
		@Override
		public boolean add(Pair<Long, OperationEnum> object) {
			boolean toReturn = super.add(object);
			
			if(size() >= MAX_RECORDS_TO_PERSIST_BATCHSIZE){
				persistToContactProvider();
				clear();
			}
			return toReturn;
		}
		
		private void persistToContactProvider(){
			
			ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
			
			for(Pair<Long, OperationEnum> contactOperation: this){
				if(contactOperation.second == OperationEnum.INSERT){
					
					Builder builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
					builder.withValue(Data.RAW_CONTACT_ID, contactOperation.first);
					builder.withValue(Data.MIMETYPE, CONTENT_ITEM_TYPE);
					
					operations.add(builder.build());
				}else{
					
					Builder builder = ContentProviderOperation.newDelete(Data.CONTENT_URI);
					builder.withSelection(Data.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?", 
						new String[]{contactOperation.first.toString(), CONTENT_ITEM_TYPE});
					
					operations.add(builder.build());
				}
			}
			
			try{
				this.contentResolver.applyBatch(ContactsContract.AUTHORITY, operations);
			}catch(RemoteException ex){
				Log.e(SynchronizeGroupsAndContacts.class.getSimpleName(), 
					"Problem synchronizing Group selections", ex);
				throw new RuntimeException(ex.getMessage());
			}catch(OperationApplicationException ex){
				Log.e(SynchronizeGroupsAndContacts.class.getSimpleName(), 
					"Problem synchronizing Group selections", ex);
				throw new RuntimeException(ex.getMessage());
			}
		}
		
		public void persistContactData(){
			persistToContactProvider();
			clear();
		}
	}
	
	private static enum OperationEnum{
		INSERT, DELETE
	}
	
	private static void logContactDebugInfo(ContentResolver contentResolver, long rawContactId, long contactId){
		
			
			/*Cursor test2 = contentResolver.query(Phone.CONTENT_URI, new String[]{Phone.DISPLAY_NAME, 
					Phone.NUMBER, Phone.CONTACT_ID, Phone.RAW_CONTACT_ID, Phone.MIMETYPE}, 
					Phone.CONTACT_ID + "=? ", 
					new String[]{ (new Long(contactId)).toString() }, null);*/
			
		Cursor test2 = contentResolver.query(Data.CONTENT_URI, new String[]{Data.DISPLAY_NAME, 
				Data.DATA1, Data.CONTACT_ID, Data.RAW_CONTACT_ID, Data.MIMETYPE}, 
				Data.CONTACT_ID + "=? ", 
				new String[]{ (new Long(contactId)).toString() }, null);
		
			Log.i(SynchronizeGroupsAndContacts.class.getSimpleName(), "Raw: " + rawContactId + 
					" contactId: " + contactId );
			while(test2.moveToNext()){
				//String number = test2.getString(1); 
				//if( number != null && !number.trim().equals("")){
					Log.i(SynchronizeGroupsAndContacts.class.getSimpleName(), "Raw: " + test2.getLong(3) + 
							" contactId: " + test2.getLong(2) + " displayname: " + 
							test2.getString(0) + " data1: " + test2.getString(1) + 
							" mimetype: " + test2.getString(4));
				//}
			}
			test2.close();
	}
	
}
